Skip to content

Allow specifying header source for client IP - #224

Merged
kevinmcconnell merged 1 commit into
mainfrom
true-client-ip-support
Jul 16, 2026
Merged

Allow specifying header source for client IP#224
kevinmcconnell merged 1 commit into
mainfrom
true-client-ip-support

Conversation

@kevinmcconnell

Copy link
Copy Markdown
Collaborator

Typically a downstreeam proxy will pass the original client IP via X-Forwarded-For, which we already handle. However some proxies use a different header. For example, Cloudflare typically sets it in True-Client-IP.

To support this, add a new --client-ip-header deploy flag which specifies the name of the header to use. When this is set, we copy the content of that header into X-Forwarded-For before logging and proxying, as if X-Forwarded-For had been set that way in the request.

Copilot AI review requested due to automatic review settings July 16, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in way to treat an alternative “original client IP” header (e.g. Cloudflare’s True-Client-IP) as the source-of-truth by rewriting X-Forwarded-For early in request handling, so logging (remote_addr) and upstream forwarding behave as if X-Forwarded-For had been set by the downstream proxy.

Changes:

  • Add ServiceOptions.ClientIPHeader and rewrite X-Forwarded-For from that trusted header when configured.
  • Add --client-ip-header deploy flag to configure the trusted header name.
  • Add a focused service test validating X-Forwarded-For rewriting and forwarding behavior.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
internal/server/service.go Introduces ClientIPHeader option and rewrites X-Forwarded-For from the configured trusted header before proxying/logging.
internal/server/service_test.go Adds a test that verifies the rewritten X-Forwarded-For value observed by the target and preserves the original trusted header.
internal/cmd/deploy.go Exposes the new behavior via --client-ip-header deploy flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kevinmcconnell
kevinmcconnell force-pushed the true-client-ip-support branch from bad4579 to 73559ec Compare July 16, 2026 12:02
Copilot AI review requested due to automatic review settings July 16, 2026 12:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@kevinmcconnell
kevinmcconnell force-pushed the true-client-ip-support branch from 73559ec to 4400c42 Compare July 16, 2026 12:16
Copilot AI review requested due to automatic review settings July 16, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Typically a downstreeam proxy will pass the original client IP via
`X-Forwarded-For`, which we already handle. However some proxies use a
different header. For example, Cloudflare typically sets it in
`True-Client-IP`.

To support this, add a new `--client-ip-header` deploy flag which
specifies the name of the header to use. When this is set, we copy the
content of that header into `X-Forwarded-For` before logging and
proxying, as if `X-Forwarded-For` had been set that way in the request.
Copilot AI review requested due to automatic review settings July 16, 2026 12:26
@kevinmcconnell
kevinmcconnell force-pushed the true-client-ip-support branch from 4400c42 to b44a731 Compare July 16, 2026 12:26
@kevinmcconnell
kevinmcconnell merged commit 97b9f2b into main Jul 16, 2026
6 checks passed
@kevinmcconnell
kevinmcconnell deleted the true-client-ip-support branch July 16, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment on lines +460 to +462
if options.ClientIPHeader != "" {
handler = WithClientIPMiddleware(options.ClientIPHeader, handler)
}
Comment thread internal/cmd/deploy.go
deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.TargetOptions.LogResponseHeaders, "log-response-header", nil, "Additional response header to log (may be specified multiple times)")
deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.ServiceOptions.ExcludeMetricsPaths, "exclude-metrics-path", nil, "Request path(s) to exclude from Prometheus metrics (may be specified multiple times)")
deployCommand.cmd.Flags().BoolVar(&deployCommand.args.TargetOptions.ForwardHeaders, "forward-headers", false, "Forward X-Forwarded headers to target (default false if TLS enabled; otherwise true)")
deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.ClientIPHeader, "client-ip-header", "", "Request header containing the original client IP; used to populate X-Forwarded-For when present")
Comment on lines +29 to +70
func TestService_ClientIPHeaderRewritesXForwardedFor(t *testing.T) {
var xForwardedFor, trueClientIP string

serviceOptions := defaultServiceOptions
serviceOptions.ClientIPHeader = "True-Client-IP"

targetOptions := defaultTargetOptions
targetOptions.ForwardHeaders = true

service := testCreateServiceWithHandler(t, serviceOptions, targetOptions,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != defaultHealthCheckConfig.Path {
xForwardedFor = r.Header.Get("X-Forwarded-For")
trueClientIP = r.Header.Get("True-Client-IP")
}
}))

req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("True-Client-IP", "203.0.113.9")
req.Header.Set("X-Forwarded-For", "6.6.6.6")

clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
require.NoError(t, err)

w := httptest.NewRecorder()
service.ServeHTTP(w, req)

require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.Equal(t, "203.0.113.9, "+clientIP, xForwardedFor)
require.Equal(t, "203.0.113.9", trueClientIP)

// Without the trusted header, the client-supplied X-Forwarded-For is
// forwarded unmodified, as usual.
req = httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("X-Forwarded-For", "6.6.6.6")

w = httptest.NewRecorder()
service.ServeHTTP(w, req)

require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.Equal(t, "6.6.6.6, "+clientIP, xForwardedFor)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants